//
//  QCloudXMLDictionary.m
//
//  Version 1.4.1
//
//  Created by Nick Lockwood on 15/11/2010.
//  Copyright 2010 Charcoal Design. All rights reserved.
//
//  Get the latest version of QCloudXMLDictionary from here:
//
//  https://github.com/nicklockwood/QCloudXMLDictionary
//
//  This software is provided 'as-is', without any express or implied
//  warranty.  In no event will the authors be held liable for any damages
//  arising from the use of this software.
//
//  Permission is granted to anyone to use this software for any purpose,
//  including commercial applications, and to alter it and redistribute it
//  freely, subject to the following restrictions:
//
//  1. The origin of this software must not be misrepresented; you must not
//  claim that you wrote the original software. If you use this software
//  in a product, an acknowledgment in the product documentation would be
//  appreciated but is not required.
//
//  2. Altered source versions must be plainly marked as such, and must not be
//  misrepresented as being the original software.
//
//  3. This notice may not be removed or altered from any source distribution.
//

#import "QCloudXMLDictionary.h"

#pragma GCC diagnostic ignored "-Wobjc-missing-property-synthesis"
#pragma GCC diagnostic ignored "-Wpartial-availability"
#pragma GCC diagnostic ignored "-Wdirect-ivar-access"
#pragma GCC diagnostic ignored "-Wformat-non-iso"
#pragma GCC diagnostic ignored "-Wgnu"

#import <Availability.h>
#if !__has_feature(objc_arc)
#error This class requires automatic reference counting
#endif

@interface QCloudXMLDictionaryParser () <NSXMLParserDelegate>

@property (nonatomic, strong) NSMutableDictionary<NSString *, id> *root;
@property (nonatomic, strong) NSMutableArray *stack;
@property (nonatomic, strong) NSMutableString *text;

@end

@implementation QCloudXMLDictionaryParser

+ (QCloudXMLDictionaryParser *)sharedInstance {
    static dispatch_once_t once;
    static QCloudXMLDictionaryParser *sharedInstance;
    dispatch_once(&once, ^{
        sharedInstance = [[QCloudXMLDictionaryParser alloc] init];
    });
    return sharedInstance;
}

- (instancetype)init {
    if ((self = [super init])) {
        _collapseTextNodes = YES;
        _stripEmptyNodes = YES;
        _trimWhiteSpace = YES;
        _alwaysUseArrays = NO;
        _preserveComments = NO;
        _wrapRootNode = NO;
    }
    return self;
}

- (id)copyWithZone:(NSZone *)zone {
    QCloudXMLDictionaryParser *copy = [[[self class] allocWithZone:zone] init];
    copy.collapseTextNodes = _collapseTextNodes;
    copy.stripEmptyNodes = _stripEmptyNodes;
    copy.trimWhiteSpace = _trimWhiteSpace;
    copy.alwaysUseArrays = _alwaysUseArrays;
    copy.preserveComments = _preserveComments;
    copy.attributesMode = _attributesMode;
    copy.nodeNameMode = _nodeNameMode;
    copy.wrapRootNode = _wrapRootNode;
    return copy;
}

- (NSDictionary<NSString *, id> *)dictionaryWithParser:(NSXMLParser *)parser {
    parser.delegate = self;
    [parser parse];
    id result = _root;
    _root = nil;
    _stack = nil;
    _text = nil;
    return result;
}

- (NSDictionary<NSString *, id> *)dictionaryWithData:(NSData *)data {
    NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
    [parser setShouldResolveExternalEntities:NO];
    return [self dictionaryWithParser:parser];
}

- (NSDictionary<NSString *, id> *)dictionaryWithString:(NSString *)string {
    NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
    return [self dictionaryWithData:data];
}

- (NSDictionary<NSString *, id> *)dictionaryWithFile:(NSString *)path {
    NSData *data = [NSData dataWithContentsOfFile:path];
    return [self dictionaryWithData:data];
}

+ (NSString *)XMLStringForNode:(id)node withNodeName:(NSString *)nodeName {
    if ([node isKindOfClass:[NSArray class]]) {
        NSMutableArray<NSString *> *nodes = [NSMutableArray arrayWithCapacity:[node count]];
        for (id individualNode in node) {
            [nodes addObject:[self XMLStringForNode:individualNode withNodeName:nodeName]];
        }
        return [nodes componentsJoinedByString:@"\n"];
    } else if ([node isKindOfClass:[NSDictionary class]]) {
        NSDictionary<NSString *, NSString *> *attributes = [(NSDictionary *)node qcxml_attributes];
        NSMutableString *attributeString = [NSMutableString string];
        [attributes enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, __unused BOOL *stop) {
            [attributeString appendFormat:@" %@=\"%@\"", key.description.QCXMLEncodedString, value.description.QCXMLEncodedString];
        }];

        NSString *innerXML = [node qcxml_innerXML];
        if (innerXML.length) {
            return [NSString stringWithFormat:@"<%1$@%2$@>%3$@</%1$@>", nodeName, attributeString, innerXML];
        } else {
            return [NSString stringWithFormat:@"<%@%@/>", nodeName, attributeString];
        }
    } else {
        return [NSString stringWithFormat:@"<%1$@>%2$@</%1$@>", nodeName, [node description].QCXMLEncodedString];
    }
}

- (void)endText {
    if (_trimWhiteSpace) {
        _text = [[_text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] mutableCopy];
    }
    if (_text.length) {
        NSMutableDictionary *top = _stack.lastObject;
        id existing = top[QCloudXMLDictionaryTextKey];
        if ([existing isKindOfClass:[NSArray class]]) {
            [existing addObject:_text];
        } else if (existing) {
            top[QCloudXMLDictionaryTextKey] = [@[ existing, _text ] mutableCopy];
        } else {
            top[QCloudXMLDictionaryTextKey] = _text;
        }
    }
    _text = nil;
}

- (void)addText:(NSString *)text {
    if (!_text) {
        _text = [NSMutableString stringWithString:text];
    } else {
        [_text appendString:text];
    }
}

- (void)parser:(__unused NSXMLParser *)parser
    didStartElement:(NSString *)elementName
       namespaceURI:(__unused NSString *)namespaceURI
      qualifiedName:(__unused NSString *)qName
         attributes:(NSDictionary *)attributeDict {
    if ([elementName isEqualToString:@"CommonPrefixes"] || [elementName isEqualToString:@"Key"]) {
        self.trimWhiteSpace = NO;
    }
    [self endText];

    NSMutableDictionary<NSString *, id> *node = [NSMutableDictionary dictionary];
    switch (_nodeNameMode) {
        case QCloudXMLDictionaryNodeNameModeRootOnly: {
            if (!_root) {
                node[QCloudXMLDictionaryNodeNameKey] = elementName;
            }
            break;
        }
        case QCloudXMLDictionaryNodeNameModeAlways: {
            node[QCloudXMLDictionaryNodeNameKey] = elementName;
            break;
        }
        case QCloudXMLDictionaryNodeNameModeNever: {
            break;
        }
    }

    if (attributeDict.count) {
        switch (_attributesMode) {
            case QCloudXMLDictionaryAttributesModePrefixed: {
                for (NSString *key in attributeDict) {
                    node[[QCloudXMLDictionaryAttributePrefix stringByAppendingString:key]] = attributeDict[key];
                }
                break;
            }
            case QCloudXMLDictionaryAttributesModeDictionary: {
                node[QCloudXMLDictionaryAttributesKey] = attributeDict;
                break;
            }
            case QCloudXMLDictionaryAttributesModeUnprefixed: {
                [node addEntriesFromDictionary:attributeDict];
                break;
            }
            case QCloudXMLDictionaryAttributesModeDiscard: {
                break;
            }
        }
    }

    if (!_root) {
        _root = node;
        _stack = [NSMutableArray arrayWithObject:node];
        if (_wrapRootNode) {
            _root = [NSMutableDictionary dictionaryWithObject:_root forKey:elementName];
            [_stack insertObject:_root atIndex:0];
        }
    } else {
        NSMutableDictionary<NSString *, id> *top = _stack.lastObject;
        id existing = top[elementName];
        if ([existing isKindOfClass:[NSArray class]]) {
            [(NSMutableArray *)existing addObject:node];
        } else if (existing) {
            top[elementName] = [@[ existing, node ] mutableCopy];
        } else if (_alwaysUseArrays) {
            top[elementName] = [NSMutableArray arrayWithObject:node];
        } else {
            top[elementName] = node;
        }
        [_stack addObject:node];
    }
}

- (NSString *)nameForNode:(NSDictionary<NSString *, id> *)node inDictionary:(NSDictionary<NSString *, id> *)dict {
    if (node.qcxml_nodeName) {
        return node.qcxml_nodeName;
    } else {
        for (NSString *name in dict) {
            id object = dict[name];
            if (object == node) {
                return name;
            } else if ([object isKindOfClass:[NSArray class]] && [(NSArray *)object containsObject:node]) {
                return name;
            }
        }
    }
    return nil;
}

- (void)parser:(__unused NSXMLParser *)parser
    didEndElement:(__unused NSString *)elementName
     namespaceURI:(__unused NSString *)namespaceURI
    qualifiedName:(__unused NSString *)qName {
    [self endText];

    NSMutableDictionary<NSString *, id> *top = _stack.lastObject;
    [_stack removeLastObject];

    if (!top.qcxml_attributes && !top.qcxml_childNodes && !top.qcxml_comments) {
        NSMutableDictionary<NSString *, id> *newTop = _stack.lastObject;
        NSString *nodeName = [self nameForNode:top inDictionary:newTop];
        if (nodeName) {
            id parentNode = newTop[nodeName];
            NSString *innerText = top.qcxml_innerText;
            if (innerText && _collapseTextNodes) {
                if ([parentNode isKindOfClass:[NSArray class]]) {
                    parentNode[[parentNode count] - 1] = innerText;
                } else {
                    newTop[nodeName] = innerText;
                }
            } else if (!innerText) {
                if (_stripEmptyNodes) {
                    if ([parentNode isKindOfClass:[NSArray class]]) {
                        [(NSMutableArray *)parentNode removeLastObject];
                    } else {
                        [newTop removeObjectForKey:nodeName];
                    }
                } else if (!_collapseTextNodes) {
                    top[QCloudXMLDictionaryTextKey] = @"";
                }
            }
        }
    }
}

- (void)parser:(__unused NSXMLParser *)parser foundCharacters:(NSString *)string {
    [self addText:string];
}

- (void)parser:(__unused NSXMLParser *)parser foundCDATA:(NSData *)CDATABlock {
    [self addText:[[NSString alloc] initWithData:CDATABlock encoding:NSUTF8StringEncoding]];
}

- (void)parser:(__unused NSXMLParser *)parser foundComment:(NSString *)comment {
    if (_preserveComments) {
        NSMutableDictionary<NSString *, id> *top = _stack.lastObject;
        NSMutableArray<NSString *> *comments = top[QCloudXMLDictionaryCommentsKey];
        if (!comments) {
            comments = [@[ comment ] mutableCopy];
            top[QCloudXMLDictionaryCommentsKey] = comments;
        } else {
            [comments addObject:comment];
        }
    }
}

@end

@implementation NSDictionary (QCloudXMLDictionary)

+ (NSDictionary<NSString *, id> *)qcxml_dictionaryWithXMLParser:(NSXMLParser *)parser {
    return [[[QCloudXMLDictionaryParser sharedInstance] copy] dictionaryWithParser:parser];
}

+ (NSDictionary<NSString *, id> *)qcxml_dictionaryWithXMLData:(NSData *)data {
    return [[[QCloudXMLDictionaryParser sharedInstance] copy] dictionaryWithData:data];
}

+ (NSDictionary<NSString *, id> *)qcxml_dictionaryWithXMLString:(NSString *)string {
    return [[[QCloudXMLDictionaryParser sharedInstance] copy] dictionaryWithString:string];
}

+ (NSDictionary<NSString *, id> *)qcxml_dictionaryWithXMLFile:(NSString *)path {
    return [[[QCloudXMLDictionaryParser sharedInstance] copy] dictionaryWithFile:path];
}

- (nullable NSDictionary<NSString *, NSString *> *)qcxml_attributes {
    NSDictionary<NSString *, NSString *> *attributes = self[QCloudXMLDictionaryAttributesKey];
    if (attributes) {
        return attributes.count ? attributes : nil;
    } else {
        NSMutableDictionary<NSString *, id> *filteredDict = [NSMutableDictionary dictionaryWithDictionary:self];
        [filteredDict removeObjectsForKeys:@[ QCloudXMLDictionaryCommentsKey, QCloudXMLDictionaryTextKey, QCloudXMLDictionaryNodeNameKey ]];
        for (NSString *key in filteredDict.allKeys) {
            [filteredDict removeObjectForKey:key];
            if ([key hasPrefix:QCloudXMLDictionaryAttributePrefix]) {
                filteredDict[[key substringFromIndex:QCloudXMLDictionaryAttributePrefix.length]] = self[key];
            }
        }
        return filteredDict.count ? filteredDict : nil;
    }
    return nil;
}

- (nullable NSDictionary *)qcxml_childNodes {
    NSMutableDictionary *filteredDict = [self mutableCopy];
    [filteredDict removeObjectsForKeys:@[
        QCloudXMLDictionaryAttributesKey, QCloudXMLDictionaryCommentsKey, QCloudXMLDictionaryTextKey, QCloudXMLDictionaryNodeNameKey
    ]];
    for (NSString *key in filteredDict.allKeys) {
        if ([key hasPrefix:QCloudXMLDictionaryAttributePrefix]) {
            [filteredDict removeObjectForKey:key];
        }
    }
    return filteredDict.count ? filteredDict : nil;
}

- (nullable NSArray *)qcxml_comments {
    return self[QCloudXMLDictionaryCommentsKey];
}

- (nullable NSString *)qcxml_nodeName {
    return self[QCloudXMLDictionaryNodeNameKey];
}

- (id)qcxml_innerText {
    id text = self[QCloudXMLDictionaryTextKey];
    if ([text isKindOfClass:[NSArray class]]) {
        return [text componentsJoinedByString:@"\n"];
    } else {
        return text;
    }
}

- (NSString *)qcxml_innerXML {
    NSMutableArray *nodes = [NSMutableArray array];

    for (NSString *comment in [self qcxml_comments]) {
        [nodes addObject:[NSString stringWithFormat:@"<!--%@-->", [comment QCXMLEncodedString]]];
    }

    NSDictionary *childNodes = [self qcxml_childNodes];
    for (NSString *key in childNodes) {
        [nodes addObject:[QCloudXMLDictionaryParser XMLStringForNode:childNodes[key] withNodeName:key]];
    }

    NSString *text = [self qcxml_innerText];
    if (text) {
        [nodes addObject:[text QCXMLEncodedString]];
    }

    return [nodes componentsJoinedByString:@"\n"];
}

- (NSString *)qcxml_XMLString {
    if (self.count == 1 && ![self qcxml_nodeName]) {
        // ignore outermost dictionary
        return [self qcxml_innerXML];
    } else {
        return [QCloudXMLDictionaryParser XMLStringForNode:self withNodeName:[self qcxml_nodeName] ?: @"root"];
    }
}

- (nullable NSArray *)qcxml_arrayValueForKeyPath:(NSString *)keyPath {
    id value = [self valueForKeyPath:keyPath];
    if (value && ![value isKindOfClass:[NSArray class]]) {
        return @[ value ];
    }
    return value;
}

- (nullable NSString *)qcxml_stringValueForKeyPath:(NSString *)keyPath {
    id value = [self valueForKeyPath:keyPath];
    if ([value isKindOfClass:[NSArray class]]) {
        value = ((NSArray *)value).firstObject;
    }
    if ([value isKindOfClass:[NSDictionary class]]) {
        return [(NSDictionary *)value qcxml_innerText];
    }
    return value;
}

- (nullable NSDictionary<NSString *, id> *)qcxml_dictionaryValueForKeyPath:(NSString *)keyPath {
    id value = [self valueForKeyPath:keyPath];
    if ([value isKindOfClass:[NSArray class]]) {
        value = [value count] ? value[0] : nil;
    }
    if ([value isKindOfClass:[NSString class]]) {
        return @{ QCloudXMLDictionaryTextKey : value };
    }
    return value;
}

@end

@implementation NSString (QCloudXMLDictionary)

- (NSString *)QCXMLEncodedString {
    return [[[[[self stringByReplacingOccurrencesOfString:@"&" withString:@"&amp;"] stringByReplacingOccurrencesOfString:@"<" withString:@"&lt;"]
        stringByReplacingOccurrencesOfString:@">"
                                  withString:@"&gt;"] stringByReplacingOccurrencesOfString:@"\""
                                                                                withString:@"&quot;"] stringByReplacingOccurrencesOfString:@"\'"
                                                                                                                                withString:@"&apos;"];
}

@end
